Skip to content

feat: upload integrity, file_message event, push mute, device presence#280

Merged
codebestia merged 3 commits into
codebestia:mainfrom
0xratnendra:feat/upload-integrity-file-event-push-mute-device-presence
Jul 2, 2026
Merged

feat: upload integrity, file_message event, push mute, device presence#280
codebestia merged 3 commits into
codebestia:mainfrom
0xratnendra:feat/upload-integrity-file-event-push-mute-device-presence

Conversation

@0xratnendra

Copy link
Copy Markdown
Contributor

closes #217
closes #232
closes #233
closes #238

Summary

Implements four features:

  1. Ciphertext SHA256 verification on message send — transport-corruption integrity check
  2. WebSocket file_message event — signals file availability without sending bytes over the socket
  3. Push notification mute respect — skips push for muted conversations and devices with push disabled
  4. Device-based user presence — derives user online/offline state from device activity, exposes lastSeen

1. Ciphertext SHA256 Verification

What

When the client sends a send_message payload, it may optionally include ciphertextSha256. The server computes SHA-256 over the stored ciphertext and rejects the message on mismatch with a clear integrity_error.

Why

This is a transport-corruption check. The primary integrity mechanism remains the AEAD tag inside the ciphertext (verified client-side at decryption time). The server-side hash catches corruption that may occur between the client and database (e.g., faulty proxies, memory errors).

Changes

  • apps/backend/src/socket/messaging.ts — added ciphertextSha256 to the payload type; computes and compares hash before inserting the message; rejects with integrity_error on mismatch

Acceptance Criteria

  • Ciphertext hash verified server-side at completion
  • Corruption produces a clear error, file stays unusable
  • Decryption-time AEAD verification documented for clients

2. WebSocket file_message Event

What

When a message with a file-type contentType (file/*, image/*, video/*, audio/*, or file) is sent, the server emits a file_message event to the conversation room carrying { messageId, conversationId, fileId }.

Why

The WebSocket carries only metadata/availability signals, never file bytes. Recipients use the file_message event to know when to fetch and decrypt a file via GET /files/:id over HTTP. Delivery and receipts reuse the standard pipeline.

Changes

  • apps/backend/src/socket/messaging.ts — after new_message broadcast, emits file_message for file-type content

Acceptance Criteria

  • WS carries only metadata; no file bytes traverse the socket
  • Recipients fetch via GET /files/:id after the event
  • Delivery/receipts reuse the standard pipeline

3. Push Notification Mute Respect

What

Before dispatching a push notification for a new message:

  • Checks conversation_members.isMutedskips muted conversations
  • Checks user_devices.push_enabledskips devices with push disabled
  • Checks online status — skips users with active WebSocket connections

Why

Muted conversations should not generate push notifications. Each device can also have a global push opt-out. These checks are applied before dispatch to avoid unnecessary wake-ups.

Changes

  • apps/backend/src/db/schema.ts — added pushEnabled column to user_devices (default true)
  • apps/backend/drizzle/0009_push_enabled.sql — migration
  • apps/backend/drizzle/meta/_journal.json — migration entry
  • apps/backend/src/services/push.ts — new push notification service using web-push with VAPID
  • apps/backend/src/socket/messaging.ts — calls sendPushForMessage after message broadcast
  • apps/backend/package.json — added web-push and @types/web-push dependencies

Configuration

Set these environment variables to enable push:

VAPID_SUBJECT=mailto:admin@clicked.app
VAPID_PUBLIC_KEY=<your-public-key>
VAPID_PRIVATE_KEY=<your-private-key>

Acceptance Criteria

  • Muted conversations produce no push
  • Global per-device opt-out honored
  • Preference checks happen before send
  • Push is best-effort (never blocks message delivery)

4. Device-Based User Presence

What

User presence is now derived from device activity rather than relying solely on Redis WebSocket tracking:

  • Online: any non-revoked device has lastSeenAt within a 90-second window
  • Offline: lastSeen reflects the most recent lastSeenAt across all devices

Exposed via:

  • GET /users/:id/presence — returns { online: boolean, lastSeen?: string }
  • WebSocket presence_update event — includes lastSeen when the user goes offline

How

  • user_devices.lastSeenAt is updated on connect, heartbeat, and disconnect
  • deriveDevicePresence() in services/presence.ts checks if any non-revoked device was active within 90s
  • The presence endpoint falls back gracefully to device-based presence when Redis is unavailable

Changes

  • apps/backend/src/services/presence.ts — added deriveDevicePresence()
  • apps/backend/src/services/heartbeat.ts — updates user_devices.lastSeenAt on heartbeat
  • apps/backend/src/index.ts — updates lastSeenAt on connect/disconnect; includes lastSeen in presence_update events
  • apps/backend/src/routes/users.ts — presence endpoint uses Redis then falls back to device-based presence
  • apps/backend/src/middleware/socketAuth.ts — exposes identityPublicKey on the socket for device lookups

Acceptance Criteria

  • User shows online while ≥1 device is online
  • lastSeen reflects the most recent device activity when offline
  • Endpoint + WS event agree

Test Results

Test Files  15 passed (15)
     Tests  129 passed (129)
  • TypeScript compilation: clean (tsc --noEmit)
  • ESLint: 0 errors (6 pre-existing warnings)
  • Prettier format: passing

- Verify ciphertext SHA256 on send_message; reject mismatches
  (AEAD tag remains primary integrity; server hash is transport check)
- Emit file_message WS event for file/image/video/audio content;
  file bytes flow over HTTP via GET /files/:id, not the socket
- Respect mute settings before push dispatch: check
  conversation_members.isMuted and per-device pushEnabled toggle
- Derive user presence from user_devices.lastSeenAt;
  online if any non-revoked device has recent activity;
  expose lastSeen via GET /users/:id/presence and presence_update WS event
@drips-wave

drips-wave Bot commented Jun 29, 2026

Copy link
Copy Markdown

@0xratnendra Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@codebestia

Copy link
Copy Markdown
Owner

GM @0xratnendra
Please resolve the conflicts.

@codebestia

Copy link
Copy Markdown
Owner

GM @0xratnendra
Please resolve the conflicts and fix the CI.

@codebestia codebestia merged commit feb52ad into codebestia:main Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants